@omnifyjp/ts 8.0.5 → 8.0.7

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/cli.js CHANGED
@@ -241,6 +241,7 @@ program
241
241
  let phpSkippedSibling = 0;
242
242
  let phpShadowedOutOfSync = 0;
243
243
  let phpShadowedAtRisk = 0;
244
+ let phpNotExtendingBase = 0;
244
245
  const skippedSiblingExamples = [];
245
246
  for (const file of phpFiles) {
246
247
  const filePath = resolve(configDir, file.path);
@@ -261,7 +262,10 @@ program
261
262
  // assignment with no error anywhere.
262
263
  const shadowResults = evaluateShadowingConflicts(existing, file.conflicts ?? []);
263
264
  if (shadowResults.length > 0) {
264
- if (shadowResults.some((r) => r.drift === 'out-of-sync')) {
265
+ if (shadowResults.some((r) => r.drift === 'not-extending-base')) {
266
+ phpNotExtendingBase++;
267
+ }
268
+ else if (shadowResults.some((r) => r.drift === 'out-of-sync')) {
265
269
  phpShadowedOutOfSync++;
266
270
  }
267
271
  else {
@@ -319,7 +323,7 @@ program
319
323
  console.log(` ${phpCreated} files created (user-editable)`);
320
324
  if (phpSkipped > 0)
321
325
  console.log(` ${phpSkipped} files skipped (already exist)`);
322
- if (phpShadowedOutOfSync > 0 || phpShadowedAtRisk > 0) {
326
+ if (phpShadowedOutOfSync > 0 || phpShadowedAtRisk > 0 || phpNotExtendingBase > 0) {
323
327
  const summaryParts = [];
324
328
  if (phpShadowedOutOfSync > 0) {
325
329
  summaryParts.push(`${phpShadowedOutOfSync} editable file(s) already out of sync with the base`);
@@ -327,6 +331,9 @@ program
327
331
  if (phpShadowedAtRisk > 0) {
328
332
  summaryParts.push(`${phpShadowedAtRisk} shadow a generated property and will drift on the next schema change`);
329
333
  }
334
+ if (phpNotExtendingBase > 0) {
335
+ summaryParts.push(`${phpNotExtendingBase} do not extend their generated base model`);
336
+ }
330
337
  console.warn(` ${summaryParts.join(', ')} (see warnings above).`);
331
338
  }
332
339
  if (phpSkippedSibling > 0) {
@@ -69,19 +69,16 @@ trait HasFiles
69
69
  return;
70
70
  }
71
71
 
72
- File::whereIn('id', $fileIds)->update([
72
+ // One UPDATE, so the attach is atomic without a caller transaction.
73
+ // increment() with extra columns, not DB::raw('sort_order + '.$startOrder):
74
+ // raw SQL is typed literal-string, and the offset is bound here.
75
+ File::whereIn('id', $fileIds)->increment('sort_order', $startOrder, [
73
76
  'fileable_type' => $this->getMorphClass(),
74
77
  'fileable_id' => $this->getKey(),
75
78
  'collection' => $collection,
76
79
  'status' => FileStatusEnum::Permanent,
77
80
  'expires_at' => null,
78
81
  ]);
79
-
80
- // increment(), not DB::raw('sort_order + '.$startOrder): raw SQL is
81
- // typed literal-string, and a column offset needs no SQL of our own.
82
- if ($startOrder !== 0) {
83
- File::whereIn('id', $fileIds)->increment('sort_order', $startOrder);
84
- }
85
82
  }
86
83
 
87
84
  /**
@@ -15,9 +15,9 @@ export declare function shadowingPropertyConflicts(baseClass: string, baseArrayV
15
15
  */
16
16
  export declare function parseRedeclaredStringArray(source: string, prop: string): string[] | null;
17
17
  export declare function formatShadowingDrift(baseValues: readonly string[], childValues: string[] | null): string;
18
- export type ShadowingDrift = 'out-of-sync' | 'extra-only' | 'in-sync' | 'unknown';
18
+ export type ShadowingDrift = 'out-of-sync' | 'extra-only' | 'in-sync' | 'unknown' | 'not-extending-base';
19
19
  export type ShadowingConflictResult = {
20
- property: ShadowingEloquentProperty;
20
+ property: ShadowingEloquentProperty | 'extends';
21
21
  message: string;
22
22
  drift: ShadowingDrift;
23
23
  };
@@ -109,6 +109,7 @@ export function shadowingPropertyConflicts(baseClass, baseArrayValues) {
109
109
  hint: `redeclares $${prop}, which REPLACES ${baseClass}::$${prop} rather than ` +
110
110
  `merging with it (PHP does not merge redeclared properties). ` +
111
111
  `${shadowingConsequence(prop)} ${shadowingFixAdvice(prop, baseClass)}`,
112
+ baseClass,
112
113
  ...(baseValues ? { baseValues } : {}),
113
114
  };
114
115
  });
@@ -117,6 +118,7 @@ export function shadowingPropertyConflicts(baseClass, baseArrayValues) {
117
118
  form: 'method',
118
119
  pattern: CASTS_METHOD_PATTERN,
119
120
  hint: shadowingCastsMethodHint(baseClass),
121
+ baseClass,
120
122
  ...(castsKeys ? { baseValues: castsKeys } : {}),
121
123
  };
122
124
  return [...propertyConflicts, methodCastsConflict];
@@ -197,9 +199,53 @@ function childValuesForConflict(existingRaw, conflict, baseValues) {
197
199
  }
198
200
  return parseRedeclaredStringArray(existingRaw, conflict.property);
199
201
  }
202
+ /**
203
+ * Issue #312: a redeclared property only REPLACES the base's when the class
204
+ * extends that base. Returns the class name and its `extends` target when the
205
+ * class does not reach `baseClass`, or null when it does (or the class
206
+ * declaration cannot be read, so the shadowing checks stay as they were).
207
+ */
208
+ function notExtendingBase(stripped, baseClass) {
209
+ const decl = stripped.match(/\bclass\s+(\w+)(?:\s+extends\s+([\\\w]+))?/);
210
+ if (!decl) {
211
+ return null;
212
+ }
213
+ const parent = decl[2] ?? null;
214
+ if (parent !== null) {
215
+ const name = parent.split('\\').pop();
216
+ if (name === baseClass) {
217
+ return null;
218
+ }
219
+ const aliased = new RegExp(String.raw `\buse\s+[\\\w]*\\` + baseClass + String.raw `\s+as\s+` + name + String.raw `\s*;`);
220
+ if (aliased.test(stripped)) {
221
+ return null;
222
+ }
223
+ }
224
+ return { className: decl[1], parent };
225
+ }
226
+ function notExtendingBaseHint(className, parent, baseClass) {
227
+ return (`${className} ${parent ? `extends ${parent}, not ${baseClass}` : `does not extend ${baseClass}`}, ` +
228
+ `so nothing omnify generates on ${baseClass} (relations, casts, $fillable, $hidden) ` +
229
+ `applies to this model. Its own $fillable / $casts replace nothing and are not checked. ` +
230
+ `Fix: extend ${baseClass}. If a parent class in between already extends it, or this ` +
231
+ `model is intentionally detached, add \`/** @omnify-base intentional */\` to silence this warning.`);
232
+ }
200
233
  export function evaluateShadowingConflicts(existingRaw, conflicts) {
201
234
  const stripped = stripPhpComments(existingRaw);
202
235
  const results = [];
236
+ const baseClass = conflicts.find((c) => c.baseClass)?.baseClass;
237
+ const detached = baseClass ? notExtendingBase(stripped, baseClass) : null;
238
+ if (baseClass && detached) {
239
+ if (existingRaw.includes('@omnify-base intentional')) {
240
+ return results;
241
+ }
242
+ results.push({
243
+ property: 'extends',
244
+ message: notExtendingBaseHint(detached.className, detached.parent, baseClass),
245
+ drift: 'not-extending-base',
246
+ });
247
+ return results;
248
+ }
203
249
  const skipsPropertyCasts = childDeclaresCastsMethod(stripped);
204
250
  for (const conflict of conflicts) {
205
251
  if (conflict.property === 'casts' && conflict.form !== 'method' && skipsPropertyCasts) {
@@ -58,6 +58,8 @@ export type ShadowingPropertyConflict = {
58
58
  readonly baseValues?: readonly string[];
59
59
  /** Property redeclaration vs method redeclaration (issue #245: casts()). */
60
60
  readonly form?: 'property' | 'method';
61
+ /** Generated base the editable class should extend (issue #312). */
62
+ readonly baseClass?: string;
61
63
  };
62
64
  /** Category of base file for modular path resolution. */
63
65
  export type BaseCategory = 'Models' | 'Controllers' | 'Requests' | 'Resources' | 'Services' | 'Enums' | 'Traits' | 'Locales' | 'Policies';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/ts",
3
- "version": "8.0.5",
3
+ "version": "8.0.7",
4
4
  "description": "TypeScript model type generator from Omnify schemas.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",