@omnifyjp/ts 4.10.0 → 5.0.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.
@@ -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
- // Convert to snake_case
237
- fields.push(propName.replace(/([A-Z])/g, (_, c, i) => (i > 0 ? '_' : '') + c.toLowerCase()));
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.getTranslatableFields(name);
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
- function generateTranslationBaseModel(name, translatableFields, config) {
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
- const fillableLines = translatableFields
30
- .map(f => ` '${f}',`)
31
- .join('\n');
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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/ts",
3
- "version": "4.10.0",
3
+ "version": "5.0.1",
4
4
  "description": "TypeScript model type generator from Omnify schemas.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",