@omnifyjp/ts 6.3.3 → 6.3.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/dist/interface-generator.js +26 -1
- package/dist/php/model-generator.js +37 -4
- package/dist/php/policy-generator.js +24 -7
- package/dist/php/resource-generator.js +11 -0
- package/dist/php/sibling-skip.d.ts +13 -0
- package/dist/php/sibling-skip.js +31 -0
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
|
@@ -62,6 +62,31 @@ function getIdType(options) {
|
|
|
62
62
|
return id;
|
|
63
63
|
return 'BigInt';
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* TS type for a MorphTo `<name>_id` column. It was hardcoded to `number`, so a
|
|
67
|
+
* morph over Uuid or Ulid targets — whose ids are strings at runtime — was typed
|
|
68
|
+
* as a number for every consumer (#176, the TS half of the same wrong
|
|
69
|
+
* assumption that emitted `unsignedBigInteger` for an all-Ulid morph).
|
|
70
|
+
*
|
|
71
|
+
* `idType:` on the property wins, as it does on the migration side; otherwise
|
|
72
|
+
* the targets decide, and a set mixing numeric and textual keys is honestly
|
|
73
|
+
* `string | number`.
|
|
74
|
+
*/
|
|
75
|
+
function morphIdTsType(property, allSchemas) {
|
|
76
|
+
const override = property.idType;
|
|
77
|
+
if (override)
|
|
78
|
+
return PK_TYPE_MAP[override] ?? 'number';
|
|
79
|
+
const types = new Set();
|
|
80
|
+
for (const target of property.targets ?? []) {
|
|
81
|
+
const schema = allSchemas[target];
|
|
82
|
+
if (!schema)
|
|
83
|
+
continue;
|
|
84
|
+
types.add(PK_TYPE_MAP[getIdType(schema.options)] ?? 'number');
|
|
85
|
+
}
|
|
86
|
+
if (types.size === 0)
|
|
87
|
+
return 'number';
|
|
88
|
+
return [...types].sort().join(' | ');
|
|
89
|
+
}
|
|
65
90
|
/** Gets TypeScript type for a property. */
|
|
66
91
|
/**
|
|
67
92
|
* The interface name to write for a relation target, or `unknown` when the
|
|
@@ -193,7 +218,7 @@ export function propertyToTSProperties(propertyName, property, schema, allSchema
|
|
|
193
218
|
},
|
|
194
219
|
{
|
|
195
220
|
name: `${propertyName}_id`,
|
|
196
|
-
type:
|
|
221
|
+
type: morphIdTsType(property, allSchemas),
|
|
197
222
|
optional: true,
|
|
198
223
|
readonly: isReadonly,
|
|
199
224
|
},
|
|
@@ -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;
|
|
@@ -550,6 +550,29 @@ function buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable
|
|
|
550
550
|
}
|
|
551
551
|
return lines.join('\n') + '\n';
|
|
552
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* PHP type for a MorphTo `<name>_id` column: `string` for Uuid/Ulid/String
|
|
555
|
+
* targets, `int` for integer ones, `int|string` when the targets disagree
|
|
556
|
+
* (#176). `idType:` on the property wins, matching the migration side.
|
|
557
|
+
*/
|
|
558
|
+
function morphIdPhpType(prop, reader) {
|
|
559
|
+
const phpFor = (idType) => idType === 'Int' || idType === 'BigInt' ? 'int' : 'string';
|
|
560
|
+
const override = prop['idType'];
|
|
561
|
+
if (typeof override === 'string' && override !== '')
|
|
562
|
+
return phpFor(override);
|
|
563
|
+
const targets = prop['targets'] ?? [];
|
|
564
|
+
const kinds = new Set();
|
|
565
|
+
for (const target of targets) {
|
|
566
|
+
const schema = reader.getSchema(target);
|
|
567
|
+
if (!schema)
|
|
568
|
+
continue;
|
|
569
|
+
const id = schema.options?.id;
|
|
570
|
+
kinds.add(phpFor(typeof id === 'string' ? id : 'BigInt'));
|
|
571
|
+
}
|
|
572
|
+
if (kinds.size === 0)
|
|
573
|
+
return 'int';
|
|
574
|
+
return [...kinds].sort().join('|');
|
|
575
|
+
}
|
|
553
576
|
function buildDocProperties(properties, expandedProperties, propertyOrder, reader, config) {
|
|
554
577
|
const lines = [];
|
|
555
578
|
// Mirror buildCasts: EnumRef columns are cast to their generated enum class,
|
|
@@ -580,7 +603,10 @@ function buildDocProperties(properties, expandedProperties, propertyOrder, reade
|
|
|
580
603
|
const nullable = prop['nullable'] ?? true;
|
|
581
604
|
const nullSuffix = nullable ? '|null' : '';
|
|
582
605
|
lines.push(` * @property string${nullSuffix} $${morphName}_type`);
|
|
583
|
-
|
|
606
|
+
// The id column follows the targets' key types (#176) — documenting it
|
|
607
|
+
// as a string when the morph is over integer keys tells static analysis
|
|
608
|
+
// the opposite of what Eloquent returns.
|
|
609
|
+
lines.push(` * @property ${morphIdPhpType(prop, reader)}${nullSuffix} $${morphName}_id`);
|
|
584
610
|
}
|
|
585
611
|
continue;
|
|
586
612
|
}
|
|
@@ -1031,10 +1057,17 @@ function buildAccessors(expandedProperties) {
|
|
|
1031
1057
|
return methods.join('\n');
|
|
1032
1058
|
}
|
|
1033
1059
|
/** Generate a user model for a package schema that extends the package's model class. */
|
|
1034
|
-
function generatePackageUserModel(name, packageNamespace, config, hasExtendAttributes = false) {
|
|
1060
|
+
function generatePackageUserModel(name, packageNamespace, config, hasExtendAttributes = false, isPivot = false) {
|
|
1035
1061
|
const modelName = toPascalCase(name);
|
|
1036
1062
|
const modelNamespace = config.models.namespace;
|
|
1037
|
-
|
|
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}`;
|
|
1038
1071
|
const traitName = extendAttributesTraitName(name);
|
|
1039
1072
|
// This file is created once and never rewritten, so the `use` line can only
|
|
1040
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 ${
|
|
131
|
-
|
|
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}(
|
|
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
|
package/dist/php/sibling-skip.js
CHANGED
|
@@ -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/dist/types.d.ts
CHANGED
|
@@ -335,6 +335,11 @@ export interface PropertyDefinition {
|
|
|
335
335
|
* integer primary key WITHOUT it gets `$incrementing = false`.
|
|
336
336
|
*/
|
|
337
337
|
readonly autoIncrement?: boolean;
|
|
338
|
+
/**
|
|
339
|
+
* Target primary key type for a relation's FK column. Honoured on MorphTo as
|
|
340
|
+
* the explicit override for the `<name>_id` column's type (#176).
|
|
341
|
+
*/
|
|
342
|
+
readonly idType?: string;
|
|
338
343
|
readonly precision?: number;
|
|
339
344
|
readonly scale?: number;
|
|
340
345
|
readonly pattern?: string;
|