@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.
- package/dist/{BlockNoteEditor-CCOSI7TW.js → BlockNoteEditor-KSPPX6JO.js} +13 -13
- package/dist/{BlockNoteEditor-CCOSI7TW.js.map → BlockNoteEditor-KSPPX6JO.js.map} +1 -1
- package/dist/{BlockNoteEditor-KH7WWHBK.mjs → BlockNoteEditor-N534QVBR.mjs} +3 -3
- package/dist/{chunk-3HL4VFJ4.js → chunk-7Z7FEMEB.js} +56 -36
- package/dist/chunk-7Z7FEMEB.js.map +1 -0
- package/dist/{chunk-KFON36OE.js → chunk-B426TLJC.js} +358 -358
- package/dist/{chunk-KFON36OE.js.map → chunk-B426TLJC.js.map} +1 -1
- package/dist/{chunk-2O7ODHTG.mjs → chunk-CK5KLBZV.mjs} +21 -1
- package/dist/chunk-CK5KLBZV.mjs.map +1 -0
- package/dist/{chunk-2TCBJO4B.mjs → chunk-TLBZWOCU.mjs} +3 -3
- package/dist/client/index.js +3 -3
- package/dist/client/index.mjs +2 -2
- package/dist/components/index.js +3 -3
- package/dist/components/index.mjs +2 -2
- package/dist/contexts/index.js +3 -3
- package/dist/contexts/index.mjs +2 -2
- package/dist/core/index.d.mts +10 -0
- package/dist/core/index.d.ts +10 -0
- package/dist/core/index.js +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/index.js +2 -2
- package/dist/index.mjs +1 -1
- package/dist/scripts/generate-web-module/templates/components/editor.template.js +108 -4
- package/dist/scripts/generate-web-module/templates/components/editor.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/data/interface.template.js +15 -2
- package/dist/scripts/generate-web-module/templates/data/interface.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/data/model.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/data/model.template.js +34 -8
- package/dist/scripts/generate-web-module/templates/data/model.template.js.map +1 -1
- package/dist/scripts/generate-web-module/transformers/i18n-generator.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/transformers/i18n-generator.js +14 -1
- package/dist/scripts/generate-web-module/transformers/i18n-generator.js.map +1 -1
- package/dist/scripts/generate-web-module/transformers/relationship-resolver.d.ts +5 -2
- package/dist/scripts/generate-web-module/transformers/relationship-resolver.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/transformers/relationship-resolver.js +17 -7
- package/dist/scripts/generate-web-module/transformers/relationship-resolver.js.map +1 -1
- package/dist/scripts/generate-web-module/types/json-schema.interface.d.ts +1 -0
- package/dist/scripts/generate-web-module/types/json-schema.interface.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/types/template-data.interface.d.ts +6 -0
- package/dist/scripts/generate-web-module/types/template-data.interface.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/utils/i18n-updater.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/utils/i18n-updater.js +3 -2
- package/dist/scripts/generate-web-module/utils/i18n-updater.js.map +1 -1
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -1
- package/scripts/generate-web-module/templates/components/editor.template.ts +115 -5
- package/scripts/generate-web-module/templates/data/interface.template.ts +18 -2
- package/scripts/generate-web-module/templates/data/model.template.ts +55 -33
- package/scripts/generate-web-module/transformers/i18n-generator.ts +16 -1
- package/scripts/generate-web-module/transformers/relationship-resolver.ts +18 -7
- package/scripts/generate-web-module/types/json-schema.interface.ts +1 -0
- package/scripts/generate-web-module/types/template-data.interface.ts +9 -0
- package/scripts/generate-web-module/utils/i18n-updater.ts +3 -2
- package/src/core/abstracts/AbstractApiData.ts +34 -0
- package/dist/chunk-2O7ODHTG.mjs.map +0 -1
- package/dist/chunk-3HL4VFJ4.js.map +0 -1
- /package/dist/{BlockNoteEditor-KH7WWHBK.mjs.map → BlockNoteEditor-N534QVBR.mjs.map} +0 -0
- /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,
|
|
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 "${
|
|
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(\`
|
|
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(\`
|
|
488
|
-
placeholder={t(\`
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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}(): ${
|
|
145
|
+
lines.push(` get ${propName}(): (${baseType}) | undefined {
|
|
138
146
|
return this._${propName};
|
|
139
147
|
}`);
|
|
140
148
|
} else {
|
|
141
|
-
lines.push(` get ${propName}(): ${
|
|
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
|
-
|
|
200
|
-
|
|
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.
|
|
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
|
-
[
|
|
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
|
|
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
|
|
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 =
|
|
82
|
-
interfaceImportPath =
|
|
83
|
-
serviceImportPath =
|
|
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 "${
|
|
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
|
-
|
|
79
|
-
|
|
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,
|