@omnifyjp/omnify 3.2.3 → 3.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/omnify",
3
- "version": "3.2.3",
3
+ "version": "3.2.5",
4
4
  "description": "Schema-driven code generation for Laravel, TypeScript, and SQL",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,10 +36,10 @@
36
36
  "zod": "^3.24.0"
37
37
  },
38
38
  "optionalDependencies": {
39
- "@omnifyjp/omnify-darwin-arm64": "3.2.3",
40
- "@omnifyjp/omnify-darwin-x64": "3.2.3",
41
- "@omnifyjp/omnify-linux-x64": "3.2.3",
42
- "@omnifyjp/omnify-linux-arm64": "3.2.3",
43
- "@omnifyjp/omnify-win32-x64": "3.2.3"
39
+ "@omnifyjp/omnify-darwin-arm64": "3.2.5",
40
+ "@omnifyjp/omnify-darwin-x64": "3.2.5",
41
+ "@omnifyjp/omnify-linux-x64": "3.2.5",
42
+ "@omnifyjp/omnify-linux-arm64": "3.2.5",
43
+ "@omnifyjp/omnify-win32-x64": "3.2.5"
44
44
  }
45
45
  }
@@ -62,7 +62,7 @@ function generateBaseModel(name, schema, reader, config) {
62
62
  const prop = properties[p];
63
63
  return prop && prop['type'] === 'File';
64
64
  });
65
- const imports = buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable, hasTranslatable, properties, needsUuidTrait, needsUlidTrait, hasNestedSet, config.nestedset.namespace, hasFiles, modelNamespace, localesNamespace, traitsNamespace, sharedModelsNamespace);
65
+ const imports = buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable, hasTranslatable, properties, needsUuidTrait, needsUlidTrait, hasNestedSet, config.nestedset.namespace, hasFiles, modelNamespace, localesNamespace, traitsNamespace, sharedModelsNamespace, config);
66
66
  const docProperties = buildDocProperties(properties, expandedProperties, propertyOrder);
67
67
  const baseClass = isAuthenticatable ? 'Authenticatable' : 'BaseModel';
68
68
  const implementsClause = hasTranslatable ? ' implements TranslatableContract' : '';
@@ -227,7 +227,7 @@ ${traits.join('\n')}
227
227
  `;
228
228
  return userFile(`${config.models.path}/${modelName}.php`, content);
229
229
  }
230
- function buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable, hasTranslatable, properties, needsUuidTrait = false, needsUlidTrait = false, hasNestedSet = false, nestedSetNamespace = 'Aimeos\\Nestedset', hasFiles = false, modelNamespace = '', localesNamespace = '', traitsNamespace = '', sharedModelsNamespace = '') {
230
+ function buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable, hasTranslatable, properties, needsUuidTrait = false, needsUlidTrait = false, hasNestedSet = false, nestedSetNamespace = 'Aimeos\\Nestedset', hasFiles = false, modelNamespace = '', localesNamespace = '', traitsNamespace = '', sharedModelsNamespace = '', config) {
231
231
  const lines = [];
232
232
  // Import BaseModel from shared namespace when in modular mode
233
233
  if (sharedModelsNamespace && sharedModelsNamespace !== baseNamespace && !isAuthenticatable) {
@@ -252,7 +252,11 @@ function buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable
252
252
  lines.push(`use ${traitsNamespace || baseNamespace + '\\Traits'}\\HasLocalizedDisplayName;`);
253
253
  lines.push(`use ${localesNamespace || baseNamespace + '\\Locales'}\\${modelName}Locales;`);
254
254
  if (hasFiles) {
255
- lines.push(`use ${traitsNamespace || baseNamespace + '\\Traits'}\\HasFiles;`);
255
+ // HasFiles lives in File module, not Shared
256
+ const fileTraitsNs = config
257
+ ? resolveModularBaseNamespace(config, 'File', 'Traits', baseNamespace + '\\Traits')
258
+ : (traitsNamespace || baseNamespace + '\\Traits');
259
+ lines.push(`use ${fileTraitsNs}\\HasFiles;`);
256
260
  if (modelNamespace) {
257
261
  lines.push(`use ${modelNamespace}\\File;`);
258
262
  }
@@ -134,6 +134,21 @@ function generateBaseRequest(name, schema, reader, config, action) {
134
134
  needsRuleImport = true;
135
135
  rulesLines.push(` '${snakeName}' => ${formatRules(rules)},`);
136
136
  attributeKeys.push(snakeName);
137
+ // Translatable: add locale-nested rules (Astrotomic expects {locale}.{field})
138
+ const isTranslatable = prop['translatable'] ?? false;
139
+ if (isTranslatable) {
140
+ const locales = reader.getLocales();
141
+ // Build nullable version of the same rules
142
+ const localeRules = rules.map(r => {
143
+ if (r === 'required')
144
+ return 'nullable';
145
+ return r;
146
+ });
147
+ for (const locale of locales) {
148
+ rulesLines.push(` '${locale}' => ['nullable', 'array'],`);
149
+ rulesLines.push(` '${locale}.${snakeName}' => ${formatRules(localeRules)},`);
150
+ }
151
+ }
137
152
  }
138
153
  const rulesContent = rulesLines.join('\n');
139
154
  const localesClass = `${modelLocalesNamespace}\\${modelName}Locales`;
@@ -67,6 +67,11 @@ function generateBaseResource(name, schema, reader, config) {
67
67
  const expr = toResourceExpression(snakeName, type);
68
68
  fields.push(` '${snakeName}' => ${expr},`);
69
69
  }
70
+ // Include translations for translatable models
71
+ const translatableFields = reader.getTranslatableFields(name);
72
+ if (translatableFields.length > 0) {
73
+ fields.push(" 'translations' => $this->getTranslationsArray(),");
74
+ }
70
75
  if (hasTimestamps) {
71
76
  fields.push(" 'created_at' => $this->created_at?->toISOString(),");
72
77
  fields.push(" 'updated_at' => $this->updated_at?->toISOString(),");
@@ -40,14 +40,15 @@ function generateBaseService(name, schema, reader, config) {
40
40
  if (!prop)
41
41
  continue;
42
42
  const colName = toSnakeCase(propName);
43
+ const translatable = prop.translatable ?? false;
43
44
  if (prop.searchable) {
44
- searchableFields.push({ propName, colName });
45
+ searchableFields.push({ propName, colName, translatable });
45
46
  }
46
47
  if (prop.filterable) {
47
- filterableFields.push({ propName, colName, type: prop.type, prop });
48
+ filterableFields.push({ propName, colName, type: prop.type, prop, translatable });
48
49
  }
49
50
  if (prop.sortable) {
50
- sortableFields.push(colName);
51
+ sortableFields.push({ colName, translatable });
51
52
  }
52
53
  }
53
54
  // Build search block
@@ -101,7 +102,12 @@ ${searchBlock}${filterBlock}${sortBlock}
101
102
  }
102
103
 
103
104
  if ($search = $filters['search'] ?? null) {
104
- $query->where(function ($q) use ($search) {${searchableFields.length > 0 ? `\n${searchableFields.map((f, i) => ` $q->${i === 0 ? 'where' : 'orWhere'}('${f.colName}', 'like', "%{$search}%");`).join('\n')}` : `\n // No searchable fields defined`}
105
+ $query->where(function ($q) use ($search) {${searchableFields.length > 0 ? `\n${searchableFields.map((f, i) => {
106
+ if (f.translatable) {
107
+ return ` $q->${i === 0 ? 'whereTranslationLike' : 'orWhereTranslationLike'}('${f.colName}', "%{$search}%");`;
108
+ }
109
+ return ` $q->${i === 0 ? 'where' : 'orWhere'}('${f.colName}', 'like', "%{$search}%");`;
110
+ }).join('\n')}` : `\n // No searchable fields defined`}
105
111
  });
106
112
  }
107
113
 
@@ -196,6 +202,10 @@ function buildSearchBlock(fields) {
196
202
  return '';
197
203
  const clauses = fields.map((f, i) => {
198
204
  const method = i === 0 ? 'where' : 'orWhere';
205
+ if (f.translatable) {
206
+ // Astrotomic scope for translatable fields
207
+ return ` $q->${i === 0 ? 'whereTranslationLike' : 'orWhereTranslationLike'}('${f.colName}', "%{$search}%");`;
208
+ }
199
209
  return ` $q->${method}('${f.colName}', 'like', "%{$search}%");`;
200
210
  });
201
211
  return `
@@ -214,7 +224,14 @@ function buildFilterBlock(fields) {
214
224
  lines.push('');
215
225
  lines.push(' // Filters');
216
226
  for (const f of fields) {
217
- const { colName, type } = f;
227
+ const { colName, type, translatable } = f;
228
+ // Translatable fields use Astrotomic scopes
229
+ if (translatable) {
230
+ lines.push(` if (isset($filters['${colName}'])) {`);
231
+ lines.push(` $query->whereTranslation('${colName}', $filters['${colName}']);`);
232
+ lines.push(` }`);
233
+ continue;
234
+ }
218
235
  switch (type) {
219
236
  case 'Decimal':
220
237
  case 'Int':
@@ -264,11 +281,22 @@ function buildSortBlock(sortableFields) {
264
281
  $query->orderBy('created_at', 'desc');
265
282
  `;
266
283
  }
267
- const allowedList = sortableFields.map(f => `'${f}'`).join(', ');
284
+ const allowedList = sortableFields.map(f => `'${f.colName}'`).join(', ');
285
+ const translatableSorts = sortableFields.filter(f => f.translatable).map(f => `'${f.colName}'`);
286
+ const translatableCheck = translatableSorts.length > 0
287
+ ? `\n $translatableSorts = [${translatableSorts.join(', ')}];`
288
+ : '';
289
+ const orderByLine = translatableSorts.length > 0
290
+ ? ` if (in_array($sortField, $translatableSorts ?? [], true)) {
291
+ $query->orderByTranslation($sortField, $direction);
292
+ } else {
293
+ $query->orderBy($sortField, $direction);
294
+ }`
295
+ : ` $query->orderBy($sortField, $direction);`;
268
296
  return `
269
297
  // Sort
270
298
  $sortParam = $filters['sort'] ?? '-created_at';
271
- $allowedSorts = [${allowedList}];
299
+ $allowedSorts = [${allowedList}];${translatableCheck}
272
300
 
273
301
  foreach (explode(',', $sortParam) as $sortField) {
274
302
  $sortField = trim($sortField);
@@ -280,7 +308,7 @@ function buildSortBlock(sortableFields) {
280
308
  }
281
309
 
282
310
  if (in_array($sortField, $allowedSorts, true) || $sortField === 'created_at' || $sortField === 'updated_at') {
283
- $query->orderBy($sortField, $direction);
311
+ ${orderByLine}
284
312
  }
285
313
  }
286
314
  `;