@omnifyjp/ts 5.0.0 → 5.0.2
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/php/model-generator.js +15 -1
- package/dist/php/relation-builder.d.ts +6 -0
- package/dist/php/relation-builder.js +7 -4
- package/dist/php/schema-reader.d.ts +15 -0
- package/dist/php/schema-reader.js +17 -2
- package/dist/php/translation-model-generator.js +61 -7
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -504,7 +504,21 @@ function buildRelations(schemaName, properties, propertyOrder, modelNamespace, r
|
|
|
504
504
|
const target = prop['target'] ?? '';
|
|
505
505
|
const ns = target ? reader.resolveModelNamespace(target, modelNamespace) : modelNamespace;
|
|
506
506
|
const targetTableName = target ? reader.getTableName(target) : '';
|
|
507
|
-
|
|
507
|
+
// For ManyToMany, look up the explicit `kind: pivot` schema (the
|
|
508
|
+
// sorted PascalCase concat of source + target — auto-created on
|
|
509
|
+
// first generate) and pass its property names so belongsToMany
|
|
510
|
+
// emits `->withPivot('col1', 'col2')`. Without this, custom pivot
|
|
511
|
+
// columns the user added are unreachable through the relation.
|
|
512
|
+
const relation = prop['relation'] ?? '';
|
|
513
|
+
let pivotProperties;
|
|
514
|
+
if (relation === 'ManyToMany' && target) {
|
|
515
|
+
const pivotName = [schemaName, target].sort().join('');
|
|
516
|
+
const pivotSchema = reader.getSchema(pivotName);
|
|
517
|
+
if (pivotSchema?.properties) {
|
|
518
|
+
pivotProperties = Object.keys(pivotSchema.properties).map(toSnakeCase);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const result = buildRelation(propName, prop, ns, { sourceTableName, targetTableName, pivotProperties });
|
|
508
522
|
if (result) {
|
|
509
523
|
methods.push('\n' + result.method);
|
|
510
524
|
declaredMethods.add(toCamelCase(propName));
|
|
@@ -8,6 +8,12 @@ interface RelationResult {
|
|
|
8
8
|
interface RelationContext {
|
|
9
9
|
sourceTableName: string;
|
|
10
10
|
targetTableName: string;
|
|
11
|
+
/** Property names declared on the explicit `kind: pivot` schema for
|
|
12
|
+
* this M2M (e.g. `["position", "notes"]`). Surfaced in the generated
|
|
13
|
+
* `belongsToMany()` chain via `->withPivot(...)` so consumers can
|
|
14
|
+
* read those columns through the relation. Empty / undefined when
|
|
15
|
+
* the user has not added custom columns to the pivot schema. */
|
|
16
|
+
pivotProperties?: string[];
|
|
11
17
|
}
|
|
12
18
|
/** Build Eloquent relation method PHP code. */
|
|
13
19
|
export declare function buildRelation(propName: string, property: Record<string, unknown>, modelNamespace: string, context?: RelationContext): RelationResult | null;
|
|
@@ -128,12 +128,15 @@ function belongsToMany(propName, property, target, ns, context) {
|
|
|
128
128
|
joinTable = generatePivotTableName(context.sourceTableName, context.targetTableName);
|
|
129
129
|
}
|
|
130
130
|
const methodName = toCamelCase(propName);
|
|
131
|
-
const pivotFields = property['pivotFields'] ?? {};
|
|
132
131
|
const fqcn = `\\${ns}\\${target}`;
|
|
133
132
|
let chain = `$this->belongsToMany(${fqcn}::class, '${joinTable}')`;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
// withPivot for every column declared on the explicit `kind: pivot`
|
|
134
|
+
// schema (resolved upstream and passed via context.pivotProperties).
|
|
135
|
+
// The legacy `pivotFields:` block on the M2M property was removed in
|
|
136
|
+
// v5.0.0; pivot columns now live on the pivot schema itself.
|
|
137
|
+
const pivotCols = context?.pivotProperties ?? [];
|
|
138
|
+
if (pivotCols.length > 0) {
|
|
139
|
+
const fields = pivotCols.map(k => `'${toSnakeCase(k)}'`).join(', ');
|
|
137
140
|
chain += `\n ->withPivot(${fields})`;
|
|
138
141
|
}
|
|
139
142
|
chain += `\n ->withTimestamps()`;
|
|
@@ -6,6 +6,9 @@ export declare class SchemaReader {
|
|
|
6
6
|
private data;
|
|
7
7
|
constructor(data: SchemasJson);
|
|
8
8
|
static fromData(data: SchemasJson): SchemaReader;
|
|
9
|
+
/** Returns the raw root document — needed by generators that read
|
|
10
|
+
* top-level config like locale.enforceLanguageFk. */
|
|
11
|
+
getRoot(): SchemasJson;
|
|
9
12
|
getSchemas(): Record<string, SchemaDefinition>;
|
|
10
13
|
getSchema(name: string): SchemaDefinition | undefined;
|
|
11
14
|
getObjectSchemas(): Record<string, SchemaDefinition>;
|
|
@@ -41,6 +44,7 @@ export declare class SchemaReader {
|
|
|
41
44
|
readonly locales: string[];
|
|
42
45
|
readonly defaultLocale: string;
|
|
43
46
|
readonly fallbackLocale: string;
|
|
47
|
+
readonly enforceLanguageFk?: boolean;
|
|
44
48
|
};
|
|
45
49
|
getLocales(): string[];
|
|
46
50
|
getDefaultLocale(): string;
|
|
@@ -72,4 +76,15 @@ export declare class SchemaReader {
|
|
|
72
76
|
hasServiceSchemas(): boolean;
|
|
73
77
|
/** Get translatable field names (snake_case) for a schema. */
|
|
74
78
|
getTranslatableFields(schemaName: string): string[];
|
|
79
|
+
/**
|
|
80
|
+
* Get translatable fields with type info (snake_case name + the
|
|
81
|
+
* declared property type). Used by translation-model-generator to
|
|
82
|
+
* emit `$casts` for non-string types — without these the model
|
|
83
|
+
* returns raw strings for Json / Boolean / Int columns and Eloquent
|
|
84
|
+
* accessors break at runtime.
|
|
85
|
+
*/
|
|
86
|
+
getTranslatableFieldDetails(schemaName: string): Array<{
|
|
87
|
+
name: string;
|
|
88
|
+
type: string;
|
|
89
|
+
}>;
|
|
75
90
|
}
|
|
@@ -9,6 +9,11 @@ export class SchemaReader {
|
|
|
9
9
|
static fromData(data) {
|
|
10
10
|
return new SchemaReader(data);
|
|
11
11
|
}
|
|
12
|
+
/** Returns the raw root document — needed by generators that read
|
|
13
|
+
* top-level config like locale.enforceLanguageFk. */
|
|
14
|
+
getRoot() {
|
|
15
|
+
return this.data;
|
|
16
|
+
}
|
|
12
17
|
// ---------------------------------------------------------------------------
|
|
13
18
|
// All schemas (project + package)
|
|
14
19
|
// ---------------------------------------------------------------------------
|
|
@@ -225,6 +230,16 @@ export class SchemaReader {
|
|
|
225
230
|
}
|
|
226
231
|
/** Get translatable field names (snake_case) for a schema. */
|
|
227
232
|
getTranslatableFields(schemaName) {
|
|
233
|
+
return this.getTranslatableFieldDetails(schemaName).map((f) => f.name);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get translatable fields with type info (snake_case name + the
|
|
237
|
+
* declared property type). Used by translation-model-generator to
|
|
238
|
+
* emit `$casts` for non-string types — without these the model
|
|
239
|
+
* returns raw strings for Json / Boolean / Int columns and Eloquent
|
|
240
|
+
* accessors break at runtime.
|
|
241
|
+
*/
|
|
242
|
+
getTranslatableFieldDetails(schemaName) {
|
|
228
243
|
const schema = this.getSchema(schemaName);
|
|
229
244
|
if (!schema?.properties)
|
|
230
245
|
return [];
|
|
@@ -233,8 +248,8 @@ export class SchemaReader {
|
|
|
233
248
|
for (const propName of propertyOrder) {
|
|
234
249
|
const prop = schema.properties[propName];
|
|
235
250
|
if (prop?.translatable) {
|
|
236
|
-
|
|
237
|
-
fields.push(
|
|
251
|
+
const snake = propName.replace(/([A-Z])/g, (_, c, i) => (i > 0 ? '_' : '') + c.toLowerCase());
|
|
252
|
+
fields.push({ name: snake, type: prop.type ?? 'String' });
|
|
238
253
|
}
|
|
239
254
|
}
|
|
240
255
|
return fields;
|
|
@@ -12,23 +12,77 @@ import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace
|
|
|
12
12
|
/** Generate translation model files for all project schemas with translatable fields. */
|
|
13
13
|
export function generateTranslationModels(reader, config) {
|
|
14
14
|
const files = [];
|
|
15
|
+
const enforceLanguageFk = reader.getRoot().locale?.enforceLanguageFk === true;
|
|
15
16
|
for (const [name] of Object.entries(reader.getProjectObjectSchemas())) {
|
|
16
|
-
const translatableFields = reader.
|
|
17
|
+
const translatableFields = reader.getTranslatableFieldDetails(name);
|
|
17
18
|
if (translatableFields.length === 0)
|
|
18
19
|
continue;
|
|
19
|
-
files.push(generateTranslationBaseModel(name, translatableFields, config));
|
|
20
|
+
files.push(generateTranslationBaseModel(name, translatableFields, enforceLanguageFk, config));
|
|
20
21
|
files.push(generateTranslationUserModel(name, config));
|
|
21
22
|
}
|
|
22
23
|
return files;
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Map an omnify property type to its Eloquent `$casts` value. Returns
|
|
27
|
+
* `undefined` for plain string types (no cast needed). Without these
|
|
28
|
+
* casts, fetching a translation row returns the raw string from the
|
|
29
|
+
* DB for Json / Boolean / Int / DateTime columns and any consumer code
|
|
30
|
+
* that assumed a typed value (e.g. `$translation->metadata['key']`)
|
|
31
|
+
* fails at runtime.
|
|
32
|
+
*/
|
|
33
|
+
function castFor(type) {
|
|
34
|
+
switch (type) {
|
|
35
|
+
case 'Json':
|
|
36
|
+
return 'array';
|
|
37
|
+
case 'Boolean':
|
|
38
|
+
return 'boolean';
|
|
39
|
+
case 'Int':
|
|
40
|
+
case 'TinyInt':
|
|
41
|
+
case 'BigInt':
|
|
42
|
+
return 'integer';
|
|
43
|
+
case 'Float':
|
|
44
|
+
return 'float';
|
|
45
|
+
case 'Decimal':
|
|
46
|
+
return 'decimal:2';
|
|
47
|
+
case 'Date':
|
|
48
|
+
return 'date';
|
|
49
|
+
case 'DateTime':
|
|
50
|
+
case 'Timestamp':
|
|
51
|
+
return 'datetime';
|
|
52
|
+
default:
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function generateTranslationBaseModel(name, translatableFields, enforceLanguageFk, config) {
|
|
25
57
|
const modelName = toPascalCase(name);
|
|
26
58
|
const modelSnake = toSnakeCase(name);
|
|
27
59
|
const baseNamespace = resolveModularBaseNamespace(config, name, 'Models', config.models.baseNamespace);
|
|
28
60
|
const tableName = `${modelSnake}_translations`;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
61
|
+
// Fillable: every translatable column + the optional `language_id`
|
|
62
|
+
// FK column (added by the migration when locale.enforceLanguageFk is
|
|
63
|
+
// on). Without including `language_id` in $fillable, Eloquent mass
|
|
64
|
+
// assignment via Astrotomic silently drops the value and the FK is
|
|
65
|
+
// never populated even though the column exists.
|
|
66
|
+
const fillableNames = translatableFields.map((f) => f.name);
|
|
67
|
+
if (enforceLanguageFk)
|
|
68
|
+
fillableNames.push('language_id');
|
|
69
|
+
const fillableLines = fillableNames.map((f) => ` '${f}',`).join('\n');
|
|
70
|
+
// Casts: only emit for fields whose type has a non-string Eloquent
|
|
71
|
+
// representation. Plain String / Text / Email / EnumRef stay as-is.
|
|
72
|
+
const castEntries = [];
|
|
73
|
+
for (const f of translatableFields) {
|
|
74
|
+
const c = castFor(f.type);
|
|
75
|
+
if (c)
|
|
76
|
+
castEntries.push(` '${f.name}' => '${c}',`);
|
|
77
|
+
}
|
|
78
|
+
const castsBlock = castEntries.length === 0 ? '' : `
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The attributes that should be cast to native types.
|
|
82
|
+
*/
|
|
83
|
+
protected $casts = [
|
|
84
|
+
${castEntries.join('\n')}
|
|
85
|
+
];`;
|
|
32
86
|
const content = `<?php
|
|
33
87
|
|
|
34
88
|
namespace ${baseNamespace};
|
|
@@ -62,7 +116,7 @@ class ${modelName}TranslationBaseModel extends Model
|
|
|
62
116
|
*/
|
|
63
117
|
protected $fillable = [
|
|
64
118
|
${fillableLines}
|
|
65
|
-
]
|
|
119
|
+
];${castsBlock}
|
|
66
120
|
}
|
|
67
121
|
`;
|
|
68
122
|
return baseFile(resolveModularBasePath(config, name, 'Models', `${modelName}TranslationBaseModel.php`, config.models.basePath), content);
|
package/dist/types.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface SchemasJson {
|
|
|
31
31
|
readonly locales: string[];
|
|
32
32
|
readonly defaultLocale: string;
|
|
33
33
|
readonly fallbackLocale: string;
|
|
34
|
+
readonly enforceLanguageFk?: boolean;
|
|
34
35
|
};
|
|
35
36
|
readonly customTypes: {
|
|
36
37
|
readonly compound: Record<string, CompoundTypeDefinition>;
|