@omnifyjp/ts 6.3.4 → 6.3.6

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.
@@ -24,7 +24,7 @@ export function generateModels(reader, config) {
24
24
  if (extended.length > 0) {
25
25
  files.push(generateExtendAttributesTrait(name, schema, extended, reader, config));
26
26
  }
27
- files.push(generatePackageUserModel(name, pkgNs, config, extended.length > 0));
27
+ files.push(generatePackageUserModel(name, pkgNs, config, extended.length > 0, schema.kind === 'pivot'));
28
28
  }
29
29
  }
30
30
  return files;
@@ -1057,10 +1057,17 @@ function buildAccessors(expandedProperties) {
1057
1057
  return methods.join('\n');
1058
1058
  }
1059
1059
  /** Generate a user model for a package schema that extends the package's model class. */
1060
- function generatePackageUserModel(name, packageNamespace, config, hasExtendAttributes = false) {
1060
+ function generatePackageUserModel(name, packageNamespace, config, hasExtendAttributes = false, isPivot = false) {
1061
1061
  const modelName = toPascalCase(name);
1062
1062
  const modelNamespace = config.models.namespace;
1063
- const packageModelClass = `${packageNamespace}\\${modelName}`;
1063
+ // #178: a `kind: pivot` schema's editable model is emitted under the
1064
+ // dedicated `Pivot/` subfolder on the package side too (v5.8.5), so the
1065
+ // parent this stub extends is `<Pkg>\Models\Pivot\<Name>`. Naming the
1066
+ // flat `<Pkg>\Models\<Name>` pointed at a class no generator ever writes,
1067
+ // and the stub fataled with "Class not found" the moment it autoloaded.
1068
+ const packageModelClass = isPivot
1069
+ ? `${packageNamespace}\\Pivot\\${modelName}`
1070
+ : `${packageNamespace}\\${modelName}`;
1064
1071
  const traitName = extendAttributesTraitName(name);
1065
1072
  // This file is created once and never rewritten, so the `use` line can only
1066
1073
  // be put here for a model that does not exist yet. A project whose model
@@ -19,6 +19,8 @@ const ACTION_METHOD_MAP = {
19
19
  };
20
20
  /** All standard actions (used for wildcard expansion). */
21
21
  const ALL_ACTIONS = ['view', 'list', 'create', 'edit', 'delete'];
22
+ /** FQCN of the acting user every policy method takes. */
23
+ const ACTOR_FQN = 'App\\Models\\User';
22
24
  /** Actions that receive both $user and $record parameters. */
23
25
  const RECORD_ACTIONS = new Set(['view', 'edit', 'delete']);
24
26
  // ============================================================================
@@ -93,6 +95,19 @@ function generateBasePolicyClass(name, schema, reader, config) {
93
95
  : (config.models.userEditableGroupByFolder
94
96
  ? nestByGroup({ path: '', namespace: config.models.userEditableNamespace }, schema.group).namespace
95
97
  : config.models.userEditableNamespace);
98
+ // Issue #177: the acting user is always `App\\Models\\User`, but the policed
99
+ // model is imported from its own namespace — and for a schema named `User`
100
+ // outside `App\\Models` the two short names collide. PHP rejects the file at
101
+ // compile time (`Cannot use ... as User because the name is already in use`),
102
+ // which is a fatal, not a catchable Throwable. Alias the actor so both stay
103
+ // distinct, and keep the signatures pointing at the class each parameter
104
+ // actually needs. When the policed model IS `App\\Models\\User` the two are
105
+ // the same class: one import, no alias.
106
+ const modelFqn = `${modelNamespace}\\${modelName}`;
107
+ const actorClass = modelName === 'User' && modelFqn !== ACTOR_FQN ? 'ActingUser' : 'User';
108
+ const actorImport = actorClass === 'User'
109
+ ? `use ${ACTOR_FQN};`
110
+ : `use ${ACTOR_FQN} as ${actorClass};`;
96
111
  // Issue #98 v5.8.14: when no Cedar-style `policies:` array is defined,
97
112
  // emit the standard CRUD scaffold with `return true;` bodies.
98
113
  // The project secures the policy by overriding methods in the
@@ -112,23 +127,23 @@ function generateBasePolicyClass(name, schema, reader, config) {
112
127
  const body = hasExplicitPolicies
113
128
  ? generateMethodBody(action, expandedPolicies, schema, reader, hasRecord)
114
129
  : ' return true;';
115
- methods.push(buildMethod(methodName, modelName, hasRecord, body));
130
+ methods.push(buildMethod(methodName, modelName, hasRecord, body, actorClass));
116
131
  }
117
132
  // SoftDelete: restore + forceDelete
118
133
  if (hasSoftDelete) {
119
134
  const restoreBody = hasExplicitPolicies
120
135
  ? generateMethodBody('delete', expandedPolicies, schema, reader)
121
136
  : ' return true;';
122
- methods.push(buildMethod('restore', modelName, true, restoreBody));
137
+ methods.push(buildMethod('restore', modelName, true, restoreBody, actorClass));
123
138
  const forceDeleteBody = hasExplicitPolicies
124
139
  ? generateMethodBody('delete', expandedPolicies, schema, reader)
125
140
  : ' return true;';
126
- methods.push(buildMethod('forceDelete', modelName, true, forceDeleteBody));
141
+ methods.push(buildMethod('forceDelete', modelName, true, forceDeleteBody, actorClass));
127
142
  }
128
143
  const cidrHelper = needsCidrHelper ? buildCidrHelper() : '';
129
144
  const imports = [...new Set([
130
- `use ${modelNamespace}\\${modelName};`,
131
- 'use App\\Models\\User;',
145
+ `use ${modelFqn};`,
146
+ actorImport,
132
147
  'use Illuminate\\Auth\\Access\\HandlesAuthorization;',
133
148
  ])].join('\n');
134
149
  const content = `<?php
@@ -280,14 +295,16 @@ function generateMethodBody(action, policies, schema, reader, hasRecord = true)
280
295
  // ============================================================================
281
296
  // Method builder
282
297
  // ============================================================================
283
- function buildMethod(methodName, modelName, hasRecord, body) {
298
+ function buildMethod(methodName, modelName, hasRecord, body,
299
+ /** Short name the acting user is imported under — aliased on collision (#177). */
300
+ actorClass = 'User') {
284
301
  const recordParam = hasRecord ? `, ${modelName} $record` : '';
285
302
  const returnType = 'bool';
286
303
  return `
287
304
  /**
288
305
  * Determine whether the user can ${methodName}.
289
306
  */
290
- public function ${methodName}(User $user${recordParam}): ${returnType}
307
+ public function ${methodName}(${actorClass} $user${recordParam}): ${returnType}
291
308
  {
292
309
  ${body}
293
310
  }`;
@@ -72,12 +72,23 @@ function generateBaseResource(name, schema, reader, config) {
72
72
  const timestamps = resolveTimestamps(options);
73
73
  const hasSoftDelete = options.softDelete ?? false;
74
74
  const hasId = options.id ?? true;
75
+ // #179: the class this file declares owns its own short name. With
76
+ // `flatBase` the base class is `<Name>Resource`, the same short name a
77
+ // self-referencing association (`parent: Department`) wants to import from
78
+ // the editable namespace — and `use App\\Http\\Resources\\DepartmentResource;`
79
+ // next to `class DepartmentResource` is a compile-time fatal. Reserving the
80
+ // declared FQN makes the importer fall back to the inline FQCN, which is the
81
+ // collision handling #174 already specified.
82
+ const declaredClassName = config.structure === 'modular'
83
+ ? `${modelName}ResourceBase`
84
+ : resolveBaseClass(config.resources, `${modelName}Resource`, 'Base').className;
75
85
  const fields = [];
76
86
  // #174: reference other Resources by short name + `use` import, the way Pint
77
87
  // would rewrite them, so `generate` and the formatter stop disagreeing.
78
88
  const importer = createClassImporter(baseNamespace, [
79
89
  'Illuminate\\Http\\Request',
80
90
  'Illuminate\\Http\\Resources\\Json\\JsonResource',
91
+ `${baseNamespace}\\${declaredClassName}`,
81
92
  ]);
82
93
  if (hasId) {
83
94
  fields.push(" 'id' => $this->id,");
@@ -34,6 +34,19 @@
34
34
  * malformed input.
35
35
  */
36
36
  export declare function findAppRootAncestor(filePath: string): string | null;
37
+ /**
38
+ * The omnify root that governs `filePath`: the closest ancestor directory
39
+ * holding an `omnify.yaml`. Null when there is none within 12 levels.
40
+ *
41
+ * Issue #178: a project with omnify packages inside `app/` (`app/Idp/`,
42
+ * `app/Auth/`, each its own omnify root) runs `generate` once per root, and
43
+ * the `app/`-wide basename scan reaches straight across those boundaries. The
44
+ * root run then skipped `app/Models/<Pivot>.php` because the package had
45
+ * emitted `app/Idp/Models/Pivot/<Pivot>.php`, and the package run skipped its
46
+ * own stub because the root had emitted the flat one — two different classes
47
+ * suppressing each other, permanently, since neither is ever rewritten.
48
+ */
49
+ export declare function findOmnifyRoot(filePath: string): string | null;
37
50
  /**
38
51
  * Scan `rootDir` recursively for a file whose basename matches
39
52
  * `baseName`, excluding `excludePath` itself. Returns the first
@@ -48,6 +48,30 @@ export function findAppRootAncestor(filePath) {
48
48
  }
49
49
  return null;
50
50
  }
51
+ /**
52
+ * The omnify root that governs `filePath`: the closest ancestor directory
53
+ * holding an `omnify.yaml`. Null when there is none within 12 levels.
54
+ *
55
+ * Issue #178: a project with omnify packages inside `app/` (`app/Idp/`,
56
+ * `app/Auth/`, each its own omnify root) runs `generate` once per root, and
57
+ * the `app/`-wide basename scan reaches straight across those boundaries. The
58
+ * root run then skipped `app/Models/<Pivot>.php` because the package had
59
+ * emitted `app/Idp/Models/Pivot/<Pivot>.php`, and the package run skipped its
60
+ * own stub because the root had emitted the flat one — two different classes
61
+ * suppressing each other, permanently, since neither is ever rewritten.
62
+ */
63
+ export function findOmnifyRoot(filePath) {
64
+ let cur = dirname(filePath);
65
+ for (let i = 0; i < 12; i++) {
66
+ if (existsSync(join(cur, 'omnify.yaml')))
67
+ return cur;
68
+ const parent = dirname(cur);
69
+ if (parent === cur)
70
+ break;
71
+ cur = parent;
72
+ }
73
+ return null;
74
+ }
51
75
  /**
52
76
  * Scan `rootDir` recursively for a file whose basename matches
53
77
  * `baseName`, excluding `excludePath` itself. Returns the first
@@ -64,6 +88,11 @@ export function findAppRootAncestor(filePath) {
64
88
  * codegen-owned", which matches `Omnify/`, `OmnifyBase/`, etc.
65
89
  */
66
90
  export function findSiblingWithSameBasename(rootDir, baseName, excludePath) {
91
+ // #178: only a file generated from the SAME omnify root can be a relocated
92
+ // version of this stub. One from another root is a different class that
93
+ // merely shares a filename, and skipping on it leaves the class nothing
94
+ // declares.
95
+ const ownerRoot = findOmnifyRoot(excludePath);
67
96
  let scanned = 0;
68
97
  const stack = [rootDir];
69
98
  while (stack.length > 0) {
@@ -104,6 +133,8 @@ export function findSiblingWithSameBasename(rootDir, baseName, excludePath) {
104
133
  stack.push(fullPath);
105
134
  }
106
135
  else if (entry === baseName && fullPath !== excludePath) {
136
+ if (findOmnifyRoot(fullPath) !== ownerRoot)
137
+ continue;
107
138
  return fullPath;
108
139
  }
109
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/ts",
3
- "version": "6.3.4",
3
+ "version": "6.3.6",
4
4
  "description": "TypeScript model type generator from Omnify schemas.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",